package net.lovesrc.mybatis.plugin;

import java.lang.management.ManagementFactory;
import java.sql.Statement;
import java.text.MessageFormat;
import java.util.List;
import java.util.Properties;

import javax.management.MBeanServer;
import javax.management.ObjectName;

import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.ParameterMode;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.SqlSessionFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.util.Assert;

@Intercepts({
	@Signature(
	    method = "query",
	    type = StatementHandler.class,
	    args = {
	    	Statement.class,
	    	ResultHandler.class
	    }
	)
})
public class PrintSqlInterceptor implements Interceptor, ApplicationContextAware, InitializingBean, PrintSqlInterceptorMBean, DisposableBean {

	private static transient final Logger logger = LoggerFactory.getLogger(PrintSqlInterceptor.class);

	private Configuration configuration;

	private ApplicationContext applicationContext;

	private String sqlSessionFactoryBeanName;

	private boolean enableJmx;

	private boolean enable = true;

	public PrintSqlInterceptor() {
	}

	@Override
	public Object intercept(Invocation invocation) throws Throwable {
		if (enable) {
			doPrintSql(invocation);
		}
		return invocation.proceed();
	}

	private void doPrintSql(Invocation invocation) {
		try {
			Object target = invocation.getTarget();
			if (! (target instanceof StatementHandler)) {
				return;
			}
			StatementHandler handler = (StatementHandler) target;
			BoundSql boundSql = handler.getBoundSql();
			List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
			int size = parameterMappings.size();
			Object[] params = new Object[size];
			Object parameterObject = boundSql.getParameterObject();
			for (int i = 0; i < parameterMappings.size(); i++) {
				ParameterMapping parameterMapping = parameterMappings.get(i);
				if (parameterMapping.getMode() != ParameterMode.OUT) {
					Object value;
					String propertyName = parameterMapping.getProperty();
					if (boundSql.hasAdditionalParameter(propertyName)) {
						value = boundSql.getAdditionalParameter(propertyName);
					} else if (parameterObject == null) {
						value = null;
					} else {
						Class<? extends Object> paramClz = parameterObject.getClass();
						if (CharSequence.class.isAssignableFrom(paramClz)
								|| Number.class.isAssignableFrom(paramClz)) {
							value = parameterObject;
						} else {
							MetaObject metaObject = getConfiguration().newMetaObject(parameterObject);
							value = metaObject.getValue(propertyName);
						}
					}
					params[i] = value;
				}
			}
			String sql = boundSql.getSql();
			sql = sql.replace("?", "'{}'");
			doPrintSql(sql, params);
		} catch (Exception e) {
			logger.error("Exception", e);
		}
	}

	private void doPrintSql(String sql, Object[] params) {
		if (logger.isInfoEnabled()) {
			logger.info(sql, params);
		} else {
			System.out.println(MessageFormat.format(sql, params));
		}
	}

	@Override
	public Object plugin(Object target) {
		return Plugin.wrap(target, this);
	}

	@Override
	public void setProperties(Properties properties) {
		// do nothing
	}

	public void setSqlSessionFactoryBeanName(String sqlSessionFactoryBeanName) {
		this.sqlSessionFactoryBeanName = sqlSessionFactoryBeanName;
	}

	public void setEnableJmx(boolean enableJmx) {
		this.enableJmx = enableJmx;
	}
	
	public void setEnable(boolean enable) {
		if (enable) {
			this.enable();
		} else {
			this.disable();
		}
	}
	
	public Configuration getConfiguration() {
		if (configuration == null) {
			Object obj = applicationContext.getBean(sqlSessionFactoryBeanName);
			if (obj instanceof SqlSessionFactory) {
				configuration = ((SqlSessionFactory) obj).getConfiguration();
			} else {
				throw new IllegalArgumentException("[id=" + sqlSessionFactoryBeanName + "] Bean must be org.apache.ibatis.session.SqlSessionFactory");
			}
		}
		return configuration;
	}
	
	@Override
	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
		this.applicationContext = applicationContext;
	}

	@Override
	public void afterPropertiesSet() throws Exception {
		if (sqlSessionFactoryBeanName == null) {
			Assert.notNull(sqlSessionFactoryBeanName, "sqlSessionFactoryBeanName is required; it must not be null");
		}
		if (!enableJmx) {
			return;
		}
		MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer();
		try {
			ObjectName name = new ObjectName(this.getClass().getName() + ":type=PrintSql");
			if (!mbeanServer.isRegistered(name)) {
				mbeanServer.registerMBean(this, name);
			}
		} catch (Exception e) {
			logger.error("Unable to start/stop JMX", e);
		}
	}

	@Override
	public boolean isEnable() {
		return false;
	}

	@Override
	public void enable() {
		this.enable = true;
	}

	@Override
	public void disable() {
		this.enable = false;
	}

	@Override
	public void destroy() throws Exception {
		if (!enableJmx) {
			return;
		}
		MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer();
		try {
			ObjectName name = new ObjectName(this.getClass().getName() + ":type=PrintSql");
			if (mbeanServer.isRegistered(name)) {
				mbeanServer.unregisterMBean(name);
			}
		} catch (Exception e) {
			logger.error("Unable to start/stop JMX", e);
		}
	}

}
